1   // Licensed under the Apache License, Version 2.0 (the "License");
2   // you may not use this file except in compliance with the License.
3   // You may obtain a copy of the License at
4   //
5   // http://www.apache.org/licenses/LICENSE-2.0
6   //
7   // Unless required by applicable law or agreed to in writing, software
8   // distributed under the License is distributed on an "AS IS" BASIS,
9   // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10  // See the License for the specific language governing permissions and
11  // limitations under the License.
12  
13  package org.apache.tapestry5.internal.pageload;
14  
15  import org.apache.tapestry5.Binding;
16  import org.apache.tapestry5.internal.services.ComponentInstantiatorSource;
17  import org.apache.tapestry5.internal.services.Instantiator;
18  import org.apache.tapestry5.internal.structure.*;
19  import org.apache.tapestry5.ioc.Invokable;
20  import org.apache.tapestry5.ioc.Location;
21  import org.apache.tapestry5.ioc.OperationTracker;
22  import org.apache.tapestry5.ioc.Resource;
23  import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
24  import org.apache.tapestry5.ioc.internal.util.InternalUtils;
25  import org.apache.tapestry5.ioc.internal.util.TapestryException;
26  import org.apache.tapestry5.ioc.util.ExceptionUtils;
27  import org.apache.tapestry5.ioc.util.IdAllocator;
28  import org.apache.tapestry5.model.ComponentModel;
29  import org.apache.tapestry5.model.EmbeddedComponentModel;
30  import org.apache.tapestry5.runtime.RenderCommand;
31  import org.apache.tapestry5.services.ComponentClassResolver;
32  import org.apache.tapestry5.services.pageload.ComponentResourceSelector;
33  
34  import java.util.List;
35  import java.util.Map;
36  
37  class ComponentAssemblerImpl implements ComponentAssembler
38  {
39      private final ComponentAssemblerSource assemblerSource;
40  
41      private final ComponentInstantiatorSource instantiatorSource;
42  
43      private final ComponentClassResolver componentClassResolver;
44  
45      private final Instantiator instantiator;
46  
47      private final ComponentPageElementResources resources;
48  
49      private final List<PageAssemblyAction> actions = CollectionFactory.newList();
50  
51      private final IdAllocator allocator = new IdAllocator();
52  
53      private final OperationTracker tracker;
54  
55      private final boolean strictMixinParameters;
56  
57      private Map<String, String> publishedParameterToEmbeddedId;
58  
59      private Map<String, EmbeddedComponentAssembler> embeddedIdToAssembler;
60  
61      public ComponentAssemblerImpl(ComponentAssemblerSource assemblerSource,
62                                    ComponentInstantiatorSource instantiatorSource, ComponentClassResolver componentClassResolver,
63                                    Instantiator instantiator, ComponentPageElementResources resources, OperationTracker tracker, boolean strictMixinParameters)
64      {
65          this.assemblerSource = assemblerSource;
66          this.instantiatorSource = instantiatorSource;
67          this.componentClassResolver = componentClassResolver;
68          this.instantiator = instantiator;
69          this.resources = resources;
70          this.tracker = tracker;
71          this.strictMixinParameters = strictMixinParameters;
72      }
73  
74      public ComponentPageElement assembleRootComponent(final Page page)
75      {
76          return tracker.invoke("Assembling root component for page " + page.getName(),
77                  new Invokable<ComponentPageElement>()
78                  {
79                      public ComponentPageElement invoke()
80                      {
81                          return performAssembleRootComponent(page);
82                      }
83                  });
84      }
85  
86      private ComponentPageElement performAssembleRootComponent(Page page)
87      {
88          PageAssembly pageAssembly = new PageAssembly(page);
89  
90          long startTime = System.currentTimeMillis();
91  
92          try
93          {
94              pageAssembly.componentCount++;
95              pageAssembly.weight++;
96  
97              ComponentPageElement newElement = new ComponentPageElementImpl(pageAssembly.page, instantiator, resources);
98  
99              pageAssembly.componentName.push(new ComponentName(pageAssembly.page.getName()));
100 
101             addRootComponentMixins(pageAssembly, newElement);
102 
103             pushNewElement(pageAssembly, newElement);
104 
105             runActions(pageAssembly);
106 
107             popNewElement(pageAssembly);
108 
109             // Execute the deferred actions in reverse order to how they were added. This makes
110             // sense, as (currently) all deferred actions are related to inheriting informal parameters;
111             // those are added deepest component to shallowest (root) component, but should be executed
112             // in the opposite order to ensure that chained inherited parameters resolve correctly.
113 
114             int count = pageAssembly.deferred.size();
115             for (int i = count - 1; i >= 0; i--)
116             {
117                 PageAssemblyAction action = pageAssembly.deferred.get(i);
118 
119                 pageAssembly.weight++;
120 
121                 action.execute(pageAssembly);
122             }
123 
124             page.setStats(new Page.Stats(System.currentTimeMillis() - startTime, pageAssembly.componentCount, pageAssembly.weight));
125 
126             return pageAssembly.createdElement.peek();
127         } catch (RuntimeException ex)
128         {
129             throw new RuntimeException(String.format("Exception assembling root component of page %s: %s", pageAssembly.page.getName(), ExceptionUtils.toMessage(ex)), ex);
130         }
131     }
132 
133     private void addRootComponentMixins(PageAssembly assembly, ComponentPageElement element)
134     {
135         for (String className : instantiator.getModel().getMixinClassNames())
136         {
137             assembly.weight++;
138 
139             Instantiator mixinInstantiator = instantiatorSource.getInstantiator(className);
140 
141             ComponentModel model = instantiator.getModel();
142             element.addMixin(InternalUtils.lastTerm(className), mixinInstantiator, model.getOrderForMixin(className));
143         }
144     }
145 
146     public void assembleEmbeddedComponent(final PageAssembly pageAssembly,
147                                           final EmbeddedComponentAssembler embeddedAssembler, final String embeddedId, final String elementName,
148                                           final Location location)
149     {
150         ComponentName containerName = pageAssembly.componentName.peek();
151 
152         final ComponentName embeddedName = containerName.child(embeddedId.toLowerCase());
153 
154         final String componentClassName = instantiator.getModel().getComponentClassName();
155 
156         String description = String.format("Assembling component %s (%s)", embeddedName.completeId, componentClassName);
157 
158         tracker.run(description, new Runnable()
159         {
160             public void run()
161             {
162                 ComponentPageElement container = pageAssembly.activeElement.peek();
163 
164                 try
165                 {
166                     pageAssembly.componentName.push(embeddedName);
167 
168                     ComponentPageElement newElement = container.newChild(embeddedId, embeddedName.nestedId,
169                             embeddedName.completeId, elementName, instantiator, location);
170 
171                     pageAssembly.componentCount++;
172                     pageAssembly.weight++;
173 
174                     pushNewElement(pageAssembly, newElement);
175 
176                     int mixinCount = embeddedAssembler.addMixinsToElement(newElement);
177 
178                     pageAssembly.weight += mixinCount;
179 
180                     runActions(pageAssembly);
181 
182                     popNewElement(pageAssembly);
183 
184                     pageAssembly.componentName.pop();
185                 } catch (RuntimeException ex)
186                 {
187                     throw new TapestryException(String.format("Exception assembling embedded component '%s' (of type %s, within %s): %s",
188                             embeddedId,
189                             componentClassName,
190                             container.getCompleteId(),
191                             ExceptionUtils.toMessage(ex)), location, ex);
192                 }
193             }
194         });
195     }
196 
197     private void pushNewElement(PageAssembly pageAssembly, final ComponentPageElement componentElement)
198     {
199         // This gets popped after all actions have executed.
200         pageAssembly.activeElement.push(componentElement);
201 
202         // The container pops this one.
203         pageAssembly.createdElement.push(componentElement);
204 
205         BodyPageElement shunt = new BodyPageElement()
206         {
207             public void addToBody(RenderCommand element)
208             {
209                 componentElement.addToTemplate(element);
210             }
211         };
212 
213         pageAssembly.bodyElement.push(shunt);
214     }
215 
216     private void popNewElement(PageAssembly pageAssembly)
217     {
218         pageAssembly.bodyElement.pop();
219         pageAssembly.activeElement.pop();
220 
221         // But the component itself stays on the createdElement stack!
222     }
223 
224     private void runActions(PageAssembly pageAssembly)
225     {
226         for (PageAssemblyAction action : actions)
227         {
228             pageAssembly.weight++;
229             action.execute(pageAssembly);
230         }
231     }
232 
233     public ComponentModel getModel()
234     {
235         return instantiator.getModel();
236     }
237 
238     public void add(PageAssemblyAction action)
239     {
240         actions.add(action);
241     }
242 
243     public void validateEmbeddedIds(Map<String, Location> componentIds, Resource templateResource)
244     {
245         Map<String, Boolean> embeddedIds = CollectionFactory.newCaseInsensitiveMap();
246 
247         for (String id : getModel().getEmbeddedComponentIds())
248             embeddedIds.put(id, true);
249 
250         for (String id : componentIds.keySet())
251         {
252             allocator.allocateId(id);
253             embeddedIds.remove(id);
254         }
255 
256         if (!embeddedIds.isEmpty())
257         {
258 
259             String className = getModel().getComponentClassName();
260 
261             throw new RuntimeException(String.format("Embedded component(s) %s are defined within component class %s (or a super-class of %s), but are not present in the component template (%s).",
262                     InternalUtils.joinSorted(embeddedIds.keySet()),
263                     className,
264                     InternalUtils.lastTerm(className),
265                     templateResource));
266         }
267     }
268 
269     public String generateEmbeddedId(String componentType)
270     {
271         // Component types may be in folders; strip off the folder part for starters.
272 
273         int slashx = componentType.lastIndexOf("/");
274 
275         String baseId = componentType.substring(slashx + 1).toLowerCase();
276 
277         // The idAllocator is pre-loaded with all the component ids from the template, so even
278         // if the lower-case type matches the id of an existing component, there won't be a name
279         // collision.
280 
281         return allocator.allocateId(baseId);
282     }
283 
284     public EmbeddedComponentAssembler createEmbeddedAssembler(String embeddedId, String componentClassName,
285                                                               EmbeddedComponentModel embeddedModel, String mixins, Location location)
286     {
287         try
288         {
289 
290             if (InternalUtils.isBlank(componentClassName))
291             {
292                 throw new TapestryException(
293                         "You must specify the type via t:type, the element, or @Component annotation.", location, null);
294             }
295 
296             EmbeddedComponentAssemblerImpl embedded = new EmbeddedComponentAssemblerImpl(assemblerSource,
297                     instantiatorSource, componentClassResolver, componentClassName, getSelector(), embeddedModel,
298                     mixins, location, strictMixinParameters);
299 
300             if (embeddedIdToAssembler == null)
301                 embeddedIdToAssembler = CollectionFactory.newMap();
302 
303             embeddedIdToAssembler.put(embeddedId, embedded);
304 
305             if (embeddedModel != null)
306             {
307                 for (String publishedParameterName : embeddedModel.getPublishedParameters())
308                 {
309                     if (publishedParameterToEmbeddedId == null)
310                         publishedParameterToEmbeddedId = CollectionFactory.newCaseInsensitiveMap();
311 
312                     String existingEmbeddedId = publishedParameterToEmbeddedId.get(publishedParameterName);
313 
314                     if (existingEmbeddedId != null)
315                     {
316                         throw new TapestryException(
317                                 String.format("Parameter '%s' of embedded component '%s' can not be published as a parameter of component %s, as it has previously been published by embedded component '%s'.",
318                                         publishedParameterName,
319                                         embeddedId,
320                                         instantiator
321                                                 .getModel().getComponentClassName(),
322                                         existingEmbeddedId), location, null);
323                     }
324 
325                     publishedParameterToEmbeddedId.put(publishedParameterName, embeddedId);
326                 }
327 
328             }
329 
330             return embedded;
331         } catch (Exception ex)
332         {
333             throw new TapestryException(String.format("Failure creating embedded component '%s' of %s: %s", embeddedId, instantiator
334                     .getModel().getComponentClassName(), ExceptionUtils.toMessage(ex)), location, ex);
335         }
336     }
337 
338     public ParameterBinder getBinder(final String parameterName)
339     {
340         final String embeddedId = InternalUtils.get(publishedParameterToEmbeddedId, parameterName);
341 
342         if (embeddedId == null)
343             return null;
344 
345         final EmbeddedComponentAssembler embededdedComponentAssembler = embeddedIdToAssembler.get(embeddedId);
346 
347         final ComponentAssembler embeddedAssembler = embededdedComponentAssembler.getComponentAssembler();
348 
349         final ParameterBinder embeddedBinder = embeddedAssembler.getBinder(parameterName);
350 
351         // The complex case: a re-publish! Yes you can go deep here if you don't
352         // value your sanity!
353 
354         if (embeddedBinder != null)
355         {
356             return new ParameterBinder()
357             {
358                 public void bind(ComponentPageElement element, Binding binding)
359                 {
360                     ComponentPageElement subelement = element.getEmbeddedElement(embeddedId);
361 
362                     embeddedBinder.bind(subelement, binding);
363                 }
364 
365                 public String getDefaultBindingPrefix(String metaDefault)
366                 {
367                     return embeddedBinder.getDefaultBindingPrefix(metaDefault);
368                 }
369             };
370         }
371 
372         final ParameterBinder innerBinder = embededdedComponentAssembler.createParameterBinder(parameterName);
373 
374         if (innerBinder == null)
375         {
376             String message = String.format("Parameter '%s' of component %s is improperly published from embedded component '%s' " +
377                     "(where it does not exist). This may be a typo in the publishParameters attribute of " +
378                     "the @Component annotation.", parameterName, instantiator.getModel()
379                     .getComponentClassName(), embeddedId);
380 
381             throw new TapestryException(message, embededdedComponentAssembler.getLocation(), null);
382         }
383         // The simple case, publishing a parameter of a subcomponent as if it were a parameter
384         // of this component.
385 
386         return new ParameterBinder()
387         {
388             public void bind(ComponentPageElement element, Binding binding)
389             {
390                 ComponentPageElement subelement = element.getEmbeddedElement(embeddedId);
391 
392                 innerBinder.bind(subelement, binding);
393             }
394 
395             public String getDefaultBindingPrefix(String metaDefault)
396             {
397                 return innerBinder.getDefaultBindingPrefix(metaDefault);
398             }
399         };
400     }
401 
402     public ComponentResourceSelector getSelector()
403     {
404         return resources.getSelector();
405     }
406 
407     @Override
408     public String toString()
409     {
410         return String.format("ComponentAssembler[%s %s]", instantiator.getModel().getComponentClassName(), getSelector());
411     }
412 }